package pl.com.qapps.unity;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

import pl.com.qapps.unity.Quicklist.Item;
import pl.com.qapps.unity.UnityPb.ItemPb.Type;

/**
 * Represents a Unity quicklist. After creating one set it to a
 * {@link UnityLauncher} by invoking
 * {@linkplain UnityLauncher#setQuicklist(Quicklist)}. <br>
 * <br>
 * There is no restriction that one item should be in only one quicklist. One
 * item can safely be in many different launcher's quicklists, but remember that
 * the action of an item can be invoked from many different launchers if so. <br>
 * <br>
 * Notice that the order items are added matters. <br>
 * <br>
 * Items can't be removed from quicklist, just make a new one without those
 * items you don't want. <br>
 * <br>
 * Sample usage code:
 * 
 * <pre>
 * <code>
 * Quicklist list = new Quicklist();
 * 
 * Item i1 = ...
 * Item i2 = ...
 * Item i3 = ...
 * 
 * list.add(i1);
 * list.add(Quicklist.SEPARATOR);
 * list.add(i2).add(i3);
 * 
 * Item[] items = ...
 * Quicklist list2 = new Quicklist(items);
 * 
 * UnityLauncher launcher = ...
 * launcher.setQuicklist(list);
 * // the quicklist set above gets replaced
 * launcher.setQuicklist(list2);
 * // or even empty
 * launcher.setQuicklist(new Quicklist());
 * //or none
 * launcher.setQuicklist(null); 
 * </code>
 * </pre>
 * 
 * @author Jakub Dykowski
 * 
 */
public final class Quicklist implements Iterable<Item> {

	/**
	 * Its an horizontal divider which separates items. Does not have label and
	 * enable/disable does not work on it.
	 */
	public static final Item SEPARATOR = new Separator();

	/**
	 * It's a base class for all types of quicklist items. <br>
	 * <br>
	 * To make an item act as real quicklist item, first add it to
	 * {@linkplain Quicklist} and then add the quicklist to
	 * {@linkplain Launcher}. <br>
	 * <br>
	 * You can change the item attributes before the quicklist gets added to
	 * launcher or even after that is done, in both cases the changes will be
	 * applied sooner or later (immediately, or after {@linkplain Quicklist}
	 * will be added to {@linkplain Launcher}) so that you can change the
	 * attributes on the fly, not just once at the beginning.
	 * 
	 * It represents single quicklist entry.
	 * 
	 * @author Jakub Dykowski
	 * 
	 */
	public static abstract class Item {

		private static final AtomicLong count = new AtomicLong(0);

		private final long id = count.incrementAndGet();
		private final PropertyChangeSupport pcs = new PropertyChangeSupport(
				this);
		private String label;
		private boolean enabled = Boolean.TRUE;
		private boolean visible = Boolean.TRUE;

		private Item(String label) throws IllegalArgumentException {

			this.label = label;
		}

		/**
		 * After this item gets clicked this method is called to immediately
		 * invoke returned {@linkplain Runnable}, if it's not <code>null</code>
		 * of course.
		 * 
		 * @return
		 */
		protected abstract Runnable getAction();

		public final Item setLabel(String label) {
			pcs.firePropertyChange("label", getLabel(), label);
			this.label = label;
			return this;
		}

		public final String getLabel() {
			return this.label;
		}

		final long getId() {
			return id;
		}

		/**
		 * If item is disabled it can't get clicked.
		 * 
		 * @param enabled
		 *            <code>true</code> if enable, <code>false</code> otherwise
		 * @return
		 * @throws LauncherException
		 *             if any internal error occurs
		 */
		public final Item setEnabled(boolean enabled) throws LauncherException {
			pcs.firePropertyChange("enabled", isEnabled(), enabled);
			this.enabled = enabled;
			return this;
		}

		/**
		 * Checks whether item is enabled or not.
		 * 
		 * @return <code>true</code> if enabled, <code>false</code> otherwise
		 */
		public final boolean isEnabled() {
			return enabled;
		}

		/**
		 * Make item visible or not.<br>
		 * <br>
		 * Instead of adding or removing items 'on the fly' which is impossible
		 * make them visible or not.
		 * 
		 * @param visible
		 *            <code>true</code> if visible, <code>false</code> otherwise
		 * @return
		 * @throws LauncherException
		 *             if any internal error occurs
		 */
		public final Item setVisible(boolean visible) throws LauncherException {
			pcs.firePropertyChange("visible", isVisible(), visible);
			this.visible = visible;
			return this;
		}

		/**
		 * Checks whether item is visible or not.
		 * 
		 * @return <code>true</code> if visible, <code>false</code> otherwise
		 */
		public final boolean isVisible() {
			return visible;
		}

		@Override
		public boolean equals(Object obj) {
			if (!(obj instanceof Item))
				return false;
			Item other = (Item) obj;
			return getId() == other.getId();
		}

		@Override
		public int hashCode() {
			return (int) (getId() ^ (getId() >>> 32));
		}

		@Override
		public final String toString() {
			return "<Item" + " " + getLabel() + "|" + getId() + ">";
		}

		void addPropertyChangeListener(PropertyChangeListener listener) {
			pcs.addPropertyChangeListener(listener);
		}

		void removePropertyChangeListener(PropertyChangeListener listener) {
			pcs.removePropertyChangeListener(listener);
		}

		public void firePropertyChange(String propertyName, boolean oldValue,
				boolean newValue) {
			pcs.firePropertyChange(propertyName, oldValue, newValue);
		}

		public void firePropertyChange(String propertyName, Object oldValue,
				Object newValue) {
			pcs.firePropertyChange(propertyName, oldValue, newValue);
		}

	}

	/**
	 * Represents an item with possibly associated action to it, or none if you
	 * like.
	 * 
	 * @author Jakub Dykowski
	 * 
	 */
	public final static class Action extends Item {

		private Runnable action;

		/**
		 * If label is <code>null</code> then "" is set. The action is being
		 * executed each time user click on the item. <br>
		 * <br>
		 * Note that it is possible to pass a <code>null</code> action as
		 * argument but then the item won't do anything after gets clicked.
		 * 
		 * @param label
		 *            can be <code>null</code>
		 * @param action
		 *            can be <code>null</code>
		 */
		public Action(String label, Runnable action) {
			super(label);
			this.action = action;
		}

		@Override
		public Runnable getAction() {
			return action;
		}
	}

	/**
	 * It is an horizontal which separates items from themselves. Label
	 * attribute does not change anything neither the enable/disable attribute.
	 * 
	 * @author Jakub Dykowski
	 * 
	 */
	static final class Separator extends Item {

		/**
		 * A horizontal divider, separating items from each other allowing for
		 * making group of items. Has got no action and {@link #getAction()}
		 * return always <code>null</code>.
		 */
		public Separator() {
			super(null);
		}

		/**
		 * Always return <code>null</code>.
		 */
		@Override
		protected Runnable getAction() {
			return null;
		}
	}

	/**
	 * It is a checkbox, which can be in two states, selected or not. To use
	 * this class inherit from it overriding its abstract method
	 * {@link #selected(boolean)}, good practice is to make anonymous classes:
	 * 
	 * <pre>
	 * <code>
	 * 	Select checkbox = new Select("Item1", true) {
	 * 		
	 * 		public void selected(boolean selected) {
	 * 			// here write your code which is activated 
	 * 			// each time the item changes its state
	 * 		}
	 * 
	 * 	}
	 * </code>
	 * </pre>
	 * 
	 * @author Jakub Dykowski
	 * 
	 */
	public static abstract class Select extends Item {

		private final Runnable action;
		private boolean selected;

		/**
		 * A shorthand for {@link #Select(label, selected)}
		 * 
		 * @param label
		 *            to be displayed, can be <code>null</code>
		 */
		public Select(String label) {
			this(label, false);
		}

		/**
		 * Represents a simple checkbox, which can be selected or not.
		 * 
		 * @param label
		 *            to be displayed, can be <code>null</code>
		 * @param selected
		 *            whether item should be selected or not
		 */
		public Select(String label, final boolean selected) {
			super(label);
			this.selected = selected;
			this.action = new Runnable() {

				@Override
				public void run() {
					synchronized (Select.this) {
						setSelected(!isSelected());
					}
				}

			};
		}

		/**
		 * This method is invoked each time item changes its state (selected or
		 * not).
		 * 
		 * @param selected
		 *            <code>true</code> if selected, <code>false</code>
		 *            otherwise
		 */
		protected abstract void selected(boolean selected);

		/**
		 * Manually set whether this item should be selected or not. If item has
		 * not been added yet it only causes the attribute to change.
		 * 
		 * @param selected
		 * @return
		 */
		public final synchronized Select setSelected(boolean selected) {
			if (isSelected() ^ selected) {
				firePropertyChange("selected", isSelected(), selected);
				this.selected = selected;
				selected(selected);
			}
			return this;
		}

		/**
		 * Checks whether item is selected or not.
		 * 
		 * @return <code>true</code> if selected, <code>false</code> otherwise
		 */
		public final boolean isSelected() {
			return this.selected;
		}

		@Override
		protected final Runnable getAction() {
			return action;
		}
	}

	/**
	 * Allows to make a group of items where just one and always one is
	 * selected, if group consists of only one item then it will be always be
	 * selected. <br>
	 * <br>
	 * In the same radio buttons group there is always one and only one selected
	 * radio button.
	 * 
	 * @author Jakub Dykowski
	 * 
	 */
	public static final class Radio<E> extends Item {

		private final RadioGroup<E> group;
		private final E option;

		/**
		 * A shorthand for Radio(label, group, <code>true</code>) Always the
		 * last item added to quicklist (in the same group) which is marked as
		 * 'selected' will be selected. if radio button is added and no item in
		 * the same group has been selected or this is the first one then it
		 * will be selected automatically.
		 * 
		 * @param label
		 * @param group
		 */
		public Radio(RadioGroup<E> group, E option) {
			this(null, group, option);
		}

		/**
		 * Always the last item added to quicklist (in the same group) which is
		 * marked as 'selected' will be selected. if radio button is added and
		 * no item in the same group has been selected or this is the first one
		 * then it will be selected automatically.
		 * 
		 * @param label
		 * @param group
		 * @param selected
		 */
		public Radio(String label, RadioGroup<E> group, E option) {
			super(label);

			if (group == null)
				throw new NullPointerException("group cannot be null");
			this.group = group;

			if (option == null)
				throw new NullPointerException("option cannot be null");
			this.option = option;

			if (group.getSelected() == null)
				group.setSelected(this);
		}

		public RadioGroup<E> getGroup() {
			return group;
		}

		@Override
		protected Runnable getAction() {
			return new Runnable() {

				@Override
				public void run() {
					if (!isSelected())
						getGroup().setSelected(Radio.this);
				}

			};
		}

		/**
		 * Checks whether item is selected or not.
		 * 
		 * @return <code>true</code> if selected, <code>false</code> otherwise
		 */
		public boolean isSelected() {
			return getGroup().getSelected() == this;
		}

		/**
		 * A simple wrapper over {@linkplain #getGroup()}.setSelected(
		 * <code>this</code>).
		 * 
		 * @return the item itself
		 */
		public Radio<E> select() {
			getGroup().setSelected(this);
			return this;
		}

		/**
		 * Returns the option that this item represents.
		 * 
		 * @return non <code>null</code>
		 */
		E getOption() {
			return option;
		}
	}

	public static abstract class RadioGroup<E> {

		// private static final AtomicLong count = new AtomicLong(1);
		// private final long id = count.getAndIncrement();
		private Radio<E> selected = null;

		protected abstract void selected(E option);

		public final Radio<E> getSelected() {
			return selected;
		}

		public final synchronized RadioGroup<E> setSelected(
				Radio<E> newSelection) {
			Radio<E> current = getSelected();
			if (current != newSelection) {
				this.selected = newSelection;
				selected(selected.getOption());
				if (current != null)
					current.firePropertyChange("selected", true, false);
				newSelection.firePropertyChange("selected", false, true);
			}
			return this;
		}

		// final long getId() {
		// return id;
		// }

	}

	private final List<Item> items = new LinkedList<>();

	/**
	 * Creates empty quicklist with no items.
	 */
	public Quicklist() {
	}

	/**
	 * Creates quicklist initially filled by items from array.
	 * 
	 * @param items
	 *            non <code>null</code> arrays of items
	 */
	public Quicklist(Item[] items) throws NullPointerException {
		if (items == null)
			throw new NullPointerException("items cannot be null");
		for (Item item : items) {
			if (item != null)
				this.items.add(item);
		}
	}

	/**
	 * Creates quicklist initially filled by given items.
	 * 
	 * @param items
	 *            non <code>null</code> {@linkplain Iterable} of items
	 */
	public Quicklist(Iterable<Item> items) {
		if (items == null)
			throw new NullPointerException("items cannot be null");
		for (Item item : items) {
			if (item != null)
				this.items.add(item);
		}
	}

	/**
	 * Return all containing items, already connected or not. You can change the
	 * attributes of those and if this quicklist is connected then updates will
	 * be shown to user otherwise it will happen when you set this quicklist to
	 * any {@linkplain UnitLauncher} instance, or couple of them. <br>
	 * <br>
	 * If you want to edit quicklist just make a new one.
	 * 
	 * 
	 * @return unmodifiable collection of items
	 */
	public List<Item> getItems() {
		return Collections.unmodifiableList(items);
	}

	/**
	 * Adds item to the quicklist. But the quicklist need to be set in launcher
	 * to make the real quicklist changed. if given item is null no action is
	 * taken.
	 * 
	 * @param item
	 *            a subclass of {@link Item}, can be <code>null</code>
	 * @return
	 */
	Quicklist add(Item item) {
		if (item != null)
			items.add(item);
		return this;
	}

	/**
	 * Items cannot be removed by returned {@linkplain Iterator}, if you attempt
	 * to do that {@linkplain UnsupportedOperationException} will be thrown.
	 */
	@Override
	public Iterator<Item> iterator() {
		return new Iterator<Item>() {

			private Iterator<Item> wrapped = items.iterator();

			@Override
			public boolean hasNext() {
				return wrapped.hasNext();
			}

			@Override
			public Item next() {
				return wrapped.next();
			}

			@Override
			public void remove() {
				throw new UnsupportedOperationException(
						"connot remove items from quicklist,"
								+ " if you really want to delete item, set new reduced"
								+ " quicklist or make the item not visible, this"
								+ " behaviour could change in the future");
			}

		};
	}

	static Type getItemType(Item item) {
		assert item != null : "cannot get item type from NULL";
		if (item instanceof Radio)
			return Type.RADIO;
		else if (item instanceof Select)
			return Type.SELECT;
		else if (item instanceof Action)
			return Type.ACTION;
		else if (item instanceof Separator)
			return Type.SEPARATOR;
		throw new RuntimeException("Implementation error, unknown item type");
	}
}
